﻿using System;
using System.Threading;
using System.Threading.Tasks;
using Utilities;

namespace Utilities
{
	public class AsyncWorker
	{
		public enum State
		{
			Stopped,
			Running,
			Suspended
		}

		/// <summary>
		/// Main worker method to do some work.
		/// </summary>
		/// <returns>true is there is more work to do, otherwise false and worker will wait until signaled with SignalWorker().</returns>
		public delegate bool AsyncWorkerJob();

		private volatile State _state = State.Stopped;
		private volatile State _requestedState = State.Stopped;

		private readonly object _lock = new object();
		private readonly EventWaiter _eventWaiter = new EventWaiter();

		private readonly AsyncWorkerJob _workerJob;

        private System.Threading.Tasks.Task _workerTask;

		public string Name { get; set; }

		public State WorkerState { get { return _state; } }

		public bool IsRunning { get { return _state == State.Running; } }
		public bool IsSuspended { get { return _state == State.Suspended; } }

		public AsyncWorker(AsyncWorkerJob workerJob)
		{
			if (workerJob == null) throw new ArgumentNullException("workerJob");
			_workerJob = workerJob;
		}

		public void Start()
		{
			lock (_lock)
			{
				if (_state == State.Stopped)
				{
					_requestedState = _state = State.Running;
					_eventWaiter.Reset();

					// http://blogs.msdn.com/b/pfxteam/archive/2010/06/13/10024153.aspx
					// prefer using Task.Factory.StartNew for .net 4.0. For .net 4.5 Task.Run is the better option.
					_workerTask = Task.Factory.StartNew(x =>
					{
						while (true)
						{
							if (_state == State.Stopped) break;

							bool haveMoreWork = false;
							if (_state == State.Running)
							{
								haveMoreWork = _workerJob();

								// Check if state has been changed in workerJob thread.
								if (_requestedState != _state && _requestedState == State.Stopped)
								{
									_state = _requestedState;
									break;
								}
							}

							if (!haveMoreWork || _state == State.Suspended) _eventWaiter.WaitOne(Timeout.Infinite);
							_state = _requestedState;
						}
					}, CancellationToken.None, TaskCreationOptions.LongRunning);

					SpinWait.SpinUntil(() => _workerTask.Status == TaskStatus.Running);
				}
				else
				{
                    //throw new InvalidOperationException("The worker is already started.");
				}
			}
		}

		public void Stop()
		{
			lock (_lock)
			{
				if (_state == State.Running || _state == State.Suspended)
				{
					_requestedState = State.Stopped;

					// Prevent deadlock by checking is we stopping from worker task or not.
					if (Task.CurrentId != _workerTask.Id)
					{
						_eventWaiter.Set();
						_workerTask.Wait();

						// http://blogs.msdn.com/b/pfxteam/archive/2012/03/25/10287435.aspx
						// Actually it's not required to call dispose on task, but we will do this if possible.
						_workerTask.Dispose();
					}
				}
				else
				{
                    //throw new InvalidOperationException("The worker is already stopped.");
				}
			}
		}

		public void Suspend()
		{
			lock (_lock)
			{
				if (_state == State.Running)
				{
					_requestedState = State.Suspended;
					_eventWaiter.Set();
					SpinWait.SpinUntil(() => _requestedState == _state);
				}
				else
				{
					// Perhaps this does not require an Exception
					//throw new InvalidOperationException("The worker is not running.");
				}
			}
		}

		public void Resume()
		{
			lock (_lock)
			{
				if (_state == State.Suspended)
				{
					_requestedState = State.Running;
					_eventWaiter.Set();
					SpinWait.SpinUntil(() => _requestedState == _state);
				}
				else
				{
					// Perhaps this does not require an Exception
					//throw new InvalidOperationException("The worker is not in suspended state.");
				}
			}
		}

		/// <summary>
		/// Signal worker to continue processing.
		/// </summary>
		public void Signal()
		{
			if (IsRunning) _eventWaiter.Set();
		}
	}
}
